/*
 * $AIST_Release: 0.9.0 $
 * Copyright 2011 Information Technology Research Institute, National
 * Institute of Advanced Industrial Science and Technology
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *    http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package generator

import java.io.File
import java.io.PrintWriter
import java.net.URLClassLoader
import java.net.URL
import java.lang.Integer

import scala.collection.JavaConversions._
import scala.collection.mutable.ListBuffer
import scala.collection.mutable.{Set => MSet}
import scala.collection.mutable.{Map=> MMap}

import com.google.protobuf.Descriptors.Descriptor
import com.google.protobuf.Descriptors.EnumDescriptor
import com.google.protobuf.ByteString

import parser.Nodes._
import parser.DataTypes._
import parser.analyzer._
import parser.Results._
import parser.TreeWalker
import parser.analyzer.TableSerializationInfoMaker.mkArrayTuple
import util.PBUtils
import util.Tools
import util.FileUtils.useFile

private[generator] case class Table(
  override val id: Int, 
  override val name: String, 
  aggr: String, 
  args: List[String], 
  sInfo: TableSerializationInfo) extends VarInfo

/**
 * Java source code generator from AST.
 */
object Generator {

  private val pkgname = "sawzallclone"
  private val usedPkg = List(
    "generator.Helpers",
    "generator.Emitter",
    "generator.BuildIn",
    "java.io.Serializable",
    "java.lang.StringBuilder",
    "java.util.Map",
    "java.util.HashMap",
    "java.util.List",
    "java.util.ArrayList",
    "java.util.Arrays",
    "java.util.Iterator",
    "aggr.SerializedTypes",
    "com.google.protobuf.Descriptors.Descriptor",
    "com.google.protobuf.Descriptors.FieldDescriptor",
    "com.google.protobuf.Descriptors.EnumDescriptor",
    "com.google.protobuf.Descriptors.EnumValueDescriptor",
    "com.google.protobuf.Message",
    "com.google.protobuf.DynamicMessage",
    "com.google.protobuf.ByteString",
    "com.google.protobuf.InvalidProtocolBufferException")

  private def printClass(pw: PrintWriter, name: String, base: Option[String], ifs: List[String])(f: => Unit) = {
    pw.print("public class %s".format(name))
    base.foreach { b =>
      pw.print(" extends %s".format(b))
    }
    if (ifs.nonEmpty) {
        pw.println(" implements %s".format(ifs.mkString(", ")))
    }
    pw.println(" {")
    f
    pw.println("}")
  }  

  /**
   * Generates a java source file.
   * 
   * @param dir  The directory where the java source file is generated.
   * @param name Class name.
   * @param base Base class.
   * @param ifs  Interfaces.
   * @param f    Block to generate class contents.
   *
   * @return Source filename and public class name(with package name).
   */
  def genSrc(dir: File, name: String, base: Option[String] = None, ifs: List[String] = List())(f: PrintWriter => Unit): (File, String) = {
    val src = new File(dir, "%s.java".format(name))
    println("Generates %s".format(src))

    useFile(new PrintWriter(src)) { pw => 
      pw.println("package %s;".format(pkgname))
      pw.println("")
      for (p <- usedPkg) {
        pw.println("import %s;".format(p))
      }

      printClass(pw, name, base, ifs) {
        f(pw)
      }
    }
    return (src, "%s.%s".format(pkgname, name))
  }

  /**
   * Generates java source files from AST.
   *
   * @param dir   The directory where the java source file is generated.
   * @param state The results of analysis.
   */
  def generate(dir: File, state: Analyzer.State): (List[TableValue], Helpers.StaticVariables) = {

    println("Generates java source file in %s".format(dir))

    val msgs  = state.fds.flatMap(PBUtils.getMessages)
    val enums = state.fds.flatMap(PBUtils.getEnums)

    val typeM = TypeMapper(state.ast, state.typeMap, state.anonymousMap)
    val enclosingVars = getEnclosingVariables(state.ast)
    val exprC = new ExprConverter(typeM, msgs, enclosingVars)
    val (svars, tables) = getStaticVariables(typeM, exprC, state.ast)

    // Generates Java source files.
    val dataTypesFile = DataTypes.generate(dir, typeM, state.ast)
    val (convertersFile, convertersClassName) = Converters.generate(dir, msgs, typeM)
    val (svarFile, svarClassName)   = StaticInitializer.genVariablesSrc(dir, svars)
    val (targsFile, targsClassName) = StaticInitializer.genTablesSrc(dir, tables)
    val mapperFile = Mapper.generate(dir, convertersClassName, svarClassName,
                                     msgs, enums, typeM, exprC, enclosingVars, 
                                     tables, state.ast)

    compile(dir, dataTypesFile, convertersFile, svarFile, targsFile, mapperFile)

    // Load
    val urls = Array(new URL("file://%s/".format(dir.getAbsolutePath)))
    val loader = new URLClassLoader(urls)

    // Statics 
    val gsvars = Class.forName(svarClassName, true, loader)
                      .getConstructor(classOf[Array[Descriptor]])
                      .newInstance(msgs.toArray)
                      .asInstanceOf[Helpers.StaticVariables]

    // TableArgs
    val tableArgs = Class.forName(targsClassName, true, loader)
                         .getConstructor(gsvars.getClass).newInstance(gsvars)
                         .asInstanceOf[Helpers.TableArguments].getValues

    val tv = tables.map { case Table(id, name, aggr, _, sInfo) =>
      val args = tableArgs.get(new Integer(id))
      TableValue(id, name, aggr, sInfo.key.toString, sInfo.value.toString, args.toList)
    }

    (tv, gsvars)
  }

  /**
   * Gets enclosing variable ID set.
   */
  def getEnclosingVariables(ast: List[Stmt]) = {
    var depth = 0
    val varDepMap = MMap[Int, Int]();
    val enclosingVars = MSet[Int]();

    TreeWalker.Walker(classOf[Node]).setPreprocess {
      case e: AnalyzedFuncLit =>
        depth += 1
        e.args.foreach {
          case a: UnusedArg => 
          case a: UsedArg => a.id
            require(!varDepMap.contains(a.id))
            varDepMap(a.id) = depth
        }
        Success(Unit)
    }.setPostprocess {
      case s: WhenVar =>
        require(!varDepMap.contains(s.id))
        varDepMap(s.id) = depth

        Success(Unit)

      case s: VarInit =>
        require(!varDepMap.contains(s.id))
        varDepMap(s.id) = depth

        Success(Unit)

      case e: LocalVariable =>
        require(varDepMap.contains(e.id))
        if (varDepMap(e.id) < depth) {
          enclosingVars += e.id
        }
        Success(Unit)
      case e: AnalyzedFuncLit =>
        depth -= 1
        Success(Unit)
    }.walk(ast)

    Set() ++ enclosingVars
  }

  /**
   * Gets static variables declarations and initializations.
   */
  def getStaticVariables(typeM: TypeMapper,
                         exprC: ExprConverter,
                         root: List[Stmt]): (List[(String, String, String)], List[Table]) = {

    var vars = new ListBuffer[(String, String, String)]
    var tables = new ListBuffer[Table]

    TreeWalker.walkTree(classOf[Node], root) {
      case s@TableInit(id, name, aggr, args, _, sInfo) =>
        // Adds to static variable table.
        val varName = staticVarName(s)

        vars += (("Helpers.Table", varName, "null"))

        // Addes to table table.
        val argsCode = args.map(exprC.convert).toList

        tables += Table(id, name, aggr, argsCode, sInfo)

        Success(Unit)
      case s@DefStaticVarStmt(_, _, initialValue) =>
        vars += ((typeM.map(initialValue.dataType),
                  staticVarName(s),
                  exprC.convert(initialValue)))
        Success(Unit)
    }

    return (vars.toList, tables.toList)
  }

  /**
   * Compiles Java source files.
   *
   * @param dir  The destination directory.
   * @param srcs The source files to be compiled.
   */
  def compile(dir: File, srcs: File*): Unit = {
    val toolsLoader = Tools.getClass.getClassLoader
    val paths = toolsLoader.asInstanceOf[URLClassLoader].getURLs.map {_.getPath} ++ List(dir.getAbsolutePath + "/")

    for (src <- srcs) {
      println("Compile %s".format(src))
      val r = Tools.javac("-cp", paths.mkString(":"), "-d", dir.toString, src.toString)

      if (r != 0) error("Fatal: compile failed.")
    }
  }

  /**
   * Gets members of Message.
   * If dataType is ArrayType, returns "elems" member.
   */
  def getMembers(dataType: DataType) = {
    Map() ++ (dataType match {
      case a: ArrayType => mkArrayTuple(a).members
      case t: TupleType => t.members
      case _ =>  error("Invalid type")
    })
  }

  /**
   * Utility function.
   * Generates Java Code to convert boolean value to long value.
   */
  def bool2long(b: String) = "(%s?1l:0l)".format(b)

  /**
   * Utility function.
   * Generates Java Code to convert long value to boolean value.
   */
  def long2bool(l: String) = "((%s) != 0l)".format(l)

  /**
   * Utility function.
   * Generates static variable name in Java code.
   */
  def staticVarName(v: VarInfo) = {
    "static_%d_%s".format(v.id, v.name)
  }

  /**
   * Utility function.
   * Generates local variable name in Java code.
   */
  def localVarName(v: VarInfo) = {
    "local_%d_%s".format(v.id, v.name)
  }

  /**
   * Utility function.
   * Generates declaration of enclosing local variable.
   */
  def mkEncVarDecl(t: String, n: String, v: String) = {
    "final Helpers.Enclosing<%s> %s = new Helpers.Enclosing<%s>(%s);".format(t, n, t, v)
  }
}
